using System;
using System.Configuration;
using System.Data;
using System.Globalization;

using BizElements.Core;

namespace BizElements.BusinessLayer
{
    #region Documentation.
    /// <summary>
    /// Manages lock information in the database.
    /// </summary>
    /// <remarks><para>Use one of the derived classes to build specialized <see cref="ILockManager"/> instances:
    /// <list type="bullet">
    /// <item><see cref="DefaultLockManager"/></item>
    /// <item><see cref="SelfLockManager"/></item>
    /// <item><see cref="DedicatedLockManager"/></item>
    /// <item><see cref="CustomLockManager"/></item>
    /// </list>
    /// </para>
    /// <para>Managers based on this class contain one or zero entries for each object that can be locked. Lock record
    /// is automatically created in a database when the object is being locked for the first time. When the lock
    /// is released the entry is not deleted. The object type and ID data is preserved. The lock owner, expiration
    /// and mode are set to nulls. Next time the object is locked, the existing data row is updated with lock info.
    /// This pattern, combined with <see cref="LockManager"/>'s internal caching mechanisms, significantly
    /// improves locking performance.</para>
    /// <para>By default the manager caches lock data acquired by the current server (see <see cref="CacheEnabled"/> 
    /// property). The cache mechanism works even when the application is hosted by a cluster. Only data for 
    /// active locks acquired on the current server is cached. The best performance is thus achieved when
    /// first acquiring the lock and then querying the manager with <see cref="ILockManager.CanRead"/> and
    /// <see cref="CanWrite"/> methods. If CanRead and CanWrite is used before the lock has been acquired,
    /// the manager will access the database to check that other server haven't acquired the lock.</para>
    /// <para>Disable cache only when you often use <see cref="ILockManager.KillLock"/> method in 
    /// a <b>server farm</b>. If a lock is acquired by one server and killed on another, it will still be
    /// active on the first server.</para>
    /// <para>Maintainance. It is a good a practice to run a database job which deletes unused lock records when 
    /// the table becomes large.</para>
    /// <para>Note to inheritors. All derived classes must be marked as serializable.</para></remarks>
    #endregion
    [Serializable]
    public class LockManager : ILockManager
    {
        #region CTor.

        /// <summary>
        /// Initializes a new instance of LockManager class.
        /// </summary>
        internal protected LockManager()
        {
        }

        /// <summary>
        /// Initializes a new instance of LockManager class.
        /// </summary>
        /// <param name="lockInfoTable">Database table which contains lock information.</param>
        /// <param name="objectTypeColumn">Database column which specifies the type of the locked object.</param>
        /// <param name="objectIdColumns">Columns which contain the ID of the locked object.</param>
        /// <param name="lockedByColumn">Nullable database field which contains the <see cref="IActor.Id"/> of <see cref="IActor"/> which locked the object.</param>
        /// <param name="lockedTillColumn">Nullable datetime database field which specifies when the lock expires.</param>
        /// <param name="lockModeColumn">Optional nullable database column which indicates whether the object is exlusively locked.</param>
        /// <param name="exclusiveLockIsDefault">Indicates whether exclusive locks are used by default.</param>
        /// <param name="exclusiveLockIndicator">The value which, when inserted into <b>lockModeColumn</b>, indicates that the object is locked exclusively.</param>
        /// <param name="sharedLockIndicator">The value which, when inserted into <b>lockModeColumn</b>, indicates that the object is locked for writing, but available for reading.</param>
        internal protected LockManager(IDbTable lockInfoTable, IDbColumn objectTypeColumn, IDbColumn[] objectIdColumns, IDbColumn lockedByColumn, IDbColumn lockedTillColumn, IDbColumn lockModeColumn, bool exclusiveLockIsDefault, object exclusiveLockIndicator, object sharedLockIndicator)
        {           
            Init(lockInfoTable, objectTypeColumn, objectIdColumns, lockedByColumn, lockedTillColumn, lockModeColumn, exclusiveLockIsDefault, exclusiveLockIndicator, sharedLockIndicator);
        }

        /// <summary>
        /// Initializes a new instance of LockManager class.
        /// </summary>
        /// <param name="lockInfoTable">Database table which contains lock information.</param>
        /// <param name="objectTypeColumn">Database column which specifies the type of the locked object.</param>
        /// <param name="objectIdColumns">Columns which contain the ID of the locked object.</param>
        /// <param name="lockedByColumn">Nullable database field which contains the <see cref="IActor.Id"/> of <see cref="IActor"/> which locked the object.</param>
        /// <param name="lockedTillColumn">Nullable datetime database field which specifies when the lock expires.</param>
        /// <param name="lockModeColumn">Optional nullable database column which indicates whether the object is exlusively locked.</param>
        /// <param name="exclusiveLockIsDefault">Indicates whether exclusive locks are used by default.</param>
        /// <param name="exclusiveLockIndicator">The value which, when inserted into <b>lockModeColumn</b>, indicates that the object is locked exclusively.</param>
        /// <param name="sharedLockIndicator">The value which, when inserted into <b>lockModeColumn</b>, indicates that the object is locked for writing, but available for reading.</param>
        internal protected void Init(IDbTable lockInfoTable, IDbColumn objectTypeColumn, IDbColumn[] objectIdColumns, IDbColumn lockedByColumn, IDbColumn lockedTillColumn, IDbColumn lockModeColumn, bool exclusiveLockIsDefault, object exclusiveLockIndicator, object sharedLockIndicator)
        {
            this.LockInfoTable = lockInfoTable;
            this.ObjectTypeColumn = objectTypeColumn;
            this.ObjectIdColumns = objectIdColumns;
            this.LockedByColumn = lockedByColumn;
            this.LockedTillColumn = lockedTillColumn;

            this.LockModeColumn = lockModeColumn;
            this.ExclusiveLockIsDefault = exclusiveLockIsDefault;
            if (exclusiveLockIndicator != null)
                this.ExclusiveLockIndicator = exclusiveLockIndicator;
            if (sharedLockIndicator != null)
                this.SharedLockIndicator = sharedLockIndicator;
        }

        #endregion

        #region Protected Internal Properties.

        IDbTable lockInfoTable;

        /// <summary>
        /// Gets or sets the database table which contains lock information. <b>Required</b>.
        /// </summary>
        protected internal IDbTable LockInfoTable
        {
            get { return lockInfoTable; }
            set { lockInfoTable = value; }
        }

        IDbColumn lockedByColumn;

        /// <summary>
        /// Gets or sets the nullable database field which contains the <see cref="IActor.Id"/> 
        /// of <see cref="IActor"/> which locked the object. <b>Required</b>.
        /// </summary>
        protected internal IDbColumn LockedByColumn
        {
            get { return lockedByColumn; }
            set
            {
                if (!value.IsNullable) throw new ArgumentException("The LockedByColumn must allow nulls.");
                lockedByColumn = value;
            }
        }

        IDbColumn lockedTillColumn;

        /// <summary>
        /// Gets or sets the nullable datetime database field which specifies when the lock expires. <b>Required</b>.
        /// </summary>
        protected internal IDbColumn LockedTillColumn
        {
            get { return lockedTillColumn; }
            set
            {
                if (!value.IsNullable) throw new ArgumentException("The LockedTillColumn must allow nulls.");
                lockedTillColumn = value;
            }
        }

        IDbColumn lockModeColumn;

        /// <summary>
        /// Gets or sets optional nullable database column which indicates whether the object is exlusively locked.
        /// </summary>
        protected internal IDbColumn LockModeColumn
        {
            get { return lockModeColumn; }
            set
            {
                if (value != null)
                    if (!value.IsNullable) throw new ArgumentException("The LockModeColumn must allow nulls.");

                lockModeColumn = value;
            }
        }

        IDbColumn[] objectIdColumns;

        /// <summary>
        /// Gets or sets the columns which contain the ID of the locked object. <b>Required</b>.
        /// </summary>
        protected internal IDbColumn[] ObjectIdColumns
        {
            get { return objectIdColumns; }
            set { objectIdColumns = value; }
        }

        IDbColumn objectTypeColumn;

        /// <summary>
        /// Gets or sets optional database column which specifies the type of the locked objects.
        /// </summary>
        /// <remarks>Must be set when using a single lock info table for multiple object types.</remarks>
        protected internal IDbColumn ObjectTypeColumn
        {
            get { return objectTypeColumn; }
            set { objectTypeColumn = value; }
        }

        bool exclusiveLockIsDefault;

        /// <summary>
        /// Gets or sets whether exclusive locks are used by default.
        /// </summary>
        protected internal bool ExclusiveLockIsDefault
        {
            get { return exclusiveLockIsDefault; }
            set { exclusiveLockIsDefault = value; }
        }

        object exclusiveLockIndicator = 1;

        /// <summary>
        /// Gets or sets the value which, when inserted into <see cref="LockModeColumn"/>, indicates that the object is locked exclusively.
        /// </summary>
        protected internal object ExclusiveLockIndicator
        {
            get { return exclusiveLockIndicator; }
            set { exclusiveLockIndicator = value; }
        }

        object sharedLockIndicator = 0;

        /// <summary>
        /// Gets or sets the value which, when inserted into <see cref="LockModeColumn"/>, indicates that the object is locked for writing, but available for reading.
        /// </summary>
        protected internal object SharedLockIndicator
        {
            get { return sharedLockIndicator; }
            set { sharedLockIndicator = value; }
        }

        static readonly TimeSpan thirtyMinutes = new TimeSpan(0, 30, 0);

        TimeSpan defaultTimeout = thirtyMinutes;

        /// <summary>
        /// Gets or sets the default lock timeout/expiration.
        /// </summary>
        /// <value>Default is 30 minutes.</value>
        protected internal TimeSpan DefaultTimeout
        {
            get { return defaultTimeout; }
            set { defaultTimeout = value; }
        }

        #endregion

        #region Public properties.

        bool cacheEnabled = true;

        /// <summary>
        /// Gets or sets whether the manager should cache lock data acquired by the current server.
        /// </summary>
        /// <value><b>true</b> to enabled internal caching; <b>false</b> otherwise. Default is <b>true</b>.</value>
        /// <remarks><para>The cache mechanism works even when the application is hosted by a cluster. Only data for 
        /// active locks acquired on the current server is cached. The best performance is thus achieved when
        /// first acquiring the lock and then querying the manager with <see cref="ILockManager.CanRead"/> and
        /// <see cref="CanWrite"/> methods. If CanRead and CanWrite is used before the lock has been acquired,
        /// the manager will access the database to check that other server haven't acquired the lock.</para>
        /// <para>Disable cache only when you often use <see cref="ILockManager.KillLock"/> method in 
        /// a <b>server farm</b>. If a lock is acquired by one server and killed on another, it will still be
        /// active on the first server.</para></remarks>
        public bool CacheEnabled
        {
            get { return cacheEnabled; }
            set { cacheEnabled = value; }
        }

        #endregion

        #region ILockManager.

        /// <summary>
        /// Tries to acquire a shared lock for the object specified in the request.
        /// </summary>
        /// <param name="owner">Actor trying to lock the object.</param>
        /// <param name="objectType">Type of the object to lock. May be null if the current instance is only used to lock a single object type.</param>
        /// <param name="objectId">ID of the object to lock.</param>
        /// <returns><b>false</b> if another <see cref="IActor"/> already locked the object; <b>true</b> otherwise.</returns>
        public virtual bool AcquireLock(IActor owner, string objectType, object[] objectId)
        {
            return AcquireLock(owner, LockMode.Default, null, objectType, objectId);
        }

        /// <summary>
        /// Tries to acquire a lock for the object specified in the request.
        /// </summary>
        /// <param name="owner">Actor trying to lock the object.</param>
        /// <param name="mode">Indicates how an object should be locked. Some managers may only support the <see cref="LockMode.Default"/> mode.</param>
        /// <param name="timeout">When will the lock expire unless prologned/reacquired. If <b>null</b> the default timeout is used.</param>
        /// <param name="objectType">Type of the object to lock. May be null if the current instance is only used to lock a single object type.</param>
        /// <param name="objectId">ID of the object to lock.</param>        
        /// <returns><b>false</b> if another <see cref="IActor"/> already locked the object; <b>true</b> otherwise.</returns>
        public virtual bool AcquireLock(IActor owner, LockMode mode, TimeSpan? timeout, string objectType, object[] objectId)
        {
            LockInfo lastLock = FetchLastLockInfo(objectType, objectId);
            bool canLock = IsActiveLock(lastLock) ? OwnsLock(owner, lastLock) : true;
            if (canLock)
            {
                TimeSpan gracePeriod = (timeout != null) ? timeout.Value : this.DefaultTimeout;
                DateTime lockedTill = Time.Now + gracePeriod;
                bool reuseOldLockInfo = (lastLock != null);
                if (reuseOldLockInfo)
                    UpdateLockInfo(owner.Id, objectType, objectId, lockedTill, GetLockModeIndicator(mode));
                else
                    InsertLockInfo(owner.Id, objectType, objectId, lockedTill, GetLockModeIndicator(mode));
            }

            return canLock;
        }

        private object GetLockModeIndicator(LockMode mode)
        {
            switch (mode)
            { 
                case LockMode.Default:
                    return (this.ExclusiveLockIsDefault) ? this.ExclusiveLockIndicator : this.SharedLockIndicator;
                case LockMode.Exclusive:
                    return this.ExclusiveLockIndicator;
                case LockMode.Shared:
                    return this.SharedLockIndicator;
                default:
                    throw new NotSupportedException("The specified lock mode is not supported. Value: " + mode.ToString());
            }
        }

        /// <summary>
        /// Checks whether a user may read the object specified in the request. 
        /// </summary>
        /// <param name="reader">Actor trying to read the object.</param>
        /// <param name="objectType">Type of the object to check. May be null if the current instance is only used to lock a single object type.</param>
        /// <param name="objectId">ID of the object to check.</param>
        /// <returns><b>false</b> if another user has acquired an exclusive lock for the object; otherwise <b>true</b>.</returns>
        public virtual bool CanRead(IActor reader, string objectType, object[] objectId)
        {
            LockInfo lastLock = FetchLastLockInfo(objectType, objectId);
            bool hasPermission;
            if (IsActiveLock(lastLock))
                hasPermission = OwnsLock(reader, lastLock) ? hasPermission = true : !lastLock.IsExclusive;
            else
                hasPermission = true;

            return hasPermission;
        }

        /// <summary>
        /// Checks whether a user may modify the object specified in the request. 
        /// </summary>
        /// <param name="owner">Actor trying to modify the object.</param>
        /// <param name="objectType">Type of the object to check. May be null if the current instance is only used to lock a single object type.</param>
        /// <param name="objectId">ID of the object to check.</param>
        /// <returns><b>true</b> if the user owns a lock for the specified object; <b>false</b> otherwise.</returns>
        public virtual bool CanWrite(IActor owner, string objectType, object[] objectId)
        {
            LockInfo lastLock = FetchLastLockInfo(objectType, objectId);
            bool hasPermission;
            if (IsActiveLock(lastLock))
                hasPermission = OwnsLock(owner, lastLock);
            else
                hasPermission = false;

            return hasPermission;
        }

        /// <summary>
        /// Forcefully cancels a lock placed on the specified object by any user.
        /// </summary>
        /// <param name="objectType">Type of the object to unlock. May be null if the current instance is only used to lock a single object type.</param>
        /// <param name="objectId">ID of the object to unlock.</param>
        /// <remarks>This method should only be available to system administrators.</remarks>
        public virtual void KillLock(string objectType, object[] objectId)
        {
            LockInfo lastLock = FetchLastLockInfo(objectType, objectId);
            if (IsActiveLock(lastLock))
                UpdateLockInfo(null, objectType, objectId, null, null);
        }

        /// <summary>
        /// Releases a lock that has previously been acquired by a user.
        /// </summary>
        /// <param name="owner">Lock owner.</param>
        /// <param name="objectType">Type of the object to unlock. May be null if the current instance is only used to lock a single object type.</param>
        /// <param name="objectId">ID of the object to unlock.</param>
        public virtual void ReleaseLock(IActor owner, string objectType, object[] objectId)
        {
            LockInfo lastLock = FetchLastLockInfo(objectType, objectId);
            if (IsActiveLock(lastLock) && OwnsLock(owner, lastLock))
                UpdateLockInfo(null, objectType, objectId, null, null);
        }

        /// <summary>
        /// Release all locks held by the specified user and managed by current <see cref="ILockManager"/>.
        /// </summary>
        /// <param name="owner">System actor.</param>
        private void ReleaseAllLocks(IActor owner)
        {
            throw new NotImplementedException();
        }

        private static bool IsActiveLock(LockInfo lastLock)
        {
            bool isActive;
            if (lastLock != null)
                isActive = (lastLock.OwnerId != null) && (lastLock.Expiration != null) && (lastLock.Expiration > Time.Now);
            else
                isActive = false;

            return isActive;
        }

        private static bool OwnsLock(IActor actor, LockInfo lastLock)
        {
            string actorId = actor.Id.ToString();
            string currentOwner = (lastLock != null && lastLock.OwnerId != null) ? lastLock.OwnerId.ToString() : null;
            bool areEqual = (actorId == currentOwner);
            return areEqual;
        }

        #endregion

        #region LockInfo - nested class.

        /// <summary>
        /// This class supports the BizElements.BusinessLayer infrastructure and is not indented to be used in your code.
        /// </summary>
        [Serializable]
        protected sealed class LockInfo
        {
            object ownerId;
            DateTime? expiration;
            bool isExclusive;

            internal object OwnerId
            {
                get { return this.ownerId; }
            }

            internal DateTime? Expiration
            {
                get { return this.expiration; }
            }

            internal bool IsExclusive
            {
                get { return this.isExclusive; }
            }

            internal LockInfo(DataRow lockData, LockManager lockSpecification)
            {
                object lockedBy = lockData[lockSpecification.LockedByColumn.ColumnName];
                this.ownerId = (lockedBy != DBNull.Value) ? lockedBy.ToString() : null;

                object lockedTill = lockData[lockSpecification.LockedTillColumn.ColumnName];
                this.expiration = (lockedTill != DBNull.Value) ? (DateTime?)Convert.ToDateTime(lockedTill) : null;
                bool useDefaultLockMode = (lockSpecification.LockModeColumn == null) || (lockData[lockSpecification.LockModeColumn.ColumnName] == DBNull.Value);
                if (useDefaultLockMode)
                {
                    this.isExclusive = lockSpecification.ExclusiveLockIsDefault;
                }
                else
                {
                    string exclusiveIndicator = Convert.ToString(lockSpecification.ExclusiveLockIndicator);
                    string lockModeValue = Convert.ToString(lockData[lockSpecification.LockModeColumn.ColumnName]);
                    this.isExclusive = (exclusiveIndicator == lockModeValue);
                }
            }

            internal LockInfo(object ownerId, DateTime? expiration, object lockModeIndicator, LockManager lockSpecification)
            {
                this.ownerId = ownerId;
                this.expiration = expiration;
                if (lockModeIndicator == null)
                {
                    this.isExclusive = lockSpecification.ExclusiveLockIsDefault;
                }
                else
                {
                    string exclusiveIndicator = Convert.ToString(lockSpecification.ExclusiveLockIndicator);
                    this.isExclusive = (exclusiveIndicator == Convert.ToString(lockModeIndicator));
                }
            }
        }

        #endregion

        #region Cached data access.

        // IMPORTANT: cache only data for active locks acquired on the current server.

        private string GetCacheKey(string objectType, object[] objectId)
        {
            return "LockManager.e563dec0b9a54546a6d642f4c532cce5_" + objectType + "_" + ObjectIdToString(objectId);
        }

        const int MinMinutesRequiredToCache = 3;

        private static int? GetCacheExpirationInMinutes(DateTime? lockedTill)
        {
            int? cacheExpirationInMinutes;
            if (lockedTill.HasValue)
            {
                int lockedForNextXMinutes = lockedTill.Value.Subtract(Time.Now).Minutes;
                cacheExpirationInMinutes = (lockedForNextXMinutes >= MinMinutesRequiredToCache) ? (int?)lockedForNextXMinutes : null;
            }
            else
            {
                cacheExpirationInMinutes = null;
            }

            return cacheExpirationInMinutes;
        }

        private LockInfo FetchLastLockInfo(string objectType, object[] objectId)
        {
            // Don't cache fetched lock data. Maybe the lock was, or will be, acquired through another server.
            // Invalidate obsolete cache entries.
            string cacheKey = GetCacheKey(objectType, objectId);
            if (GlobalCache.Contains(cacheKey))
            {
                LockInfo cachedLock = GlobalCache.GetData(cacheKey) as LockInfo;
                if (!IsActiveLock(cachedLock))
                    GlobalCache.Remove(cacheKey);
                else
                    return cachedLock;
            }

            return RpcHelper.InvokeMethod<LockInfo, string, object[]>(this, DbFetchLastLockInfo, objectType, objectId);
        }

        /// <summary>
        /// This method supports the BizElements.BusinessLayer infrastructure and is not indented to be used in your code.
        /// </summary>
        protected LockInfo DbFetchLastLockInfo(string objectType, object[] objectId)
        {
            // Don't cache fetched lock data. Maybe the lock was, or will be, acquired through another server.
            // Invalidate obsolete cache entries.
            string cacheKey = GetCacheKey(objectType, objectId);
            if (GlobalCache.Contains(cacheKey))
            {
                LockInfo cachedLock = GlobalCache.GetData(cacheKey) as LockInfo;
                if (!IsActiveLock(cachedLock))
                    GlobalCache.Remove(cacheKey);
                else
                    return cachedLock;
            }

            EnsureRequiredPropertiesAreSet();
            SelectStatement select = new SelectStatement(this.LockInfoTable);
            select.SelectList.Add(this.LockedByColumn);
            select.SelectList.Add(this.LockedTillColumn);
            if (this.LockModeColumn != null)
                select.SelectList.Add(this.LockModeColumn);

            if (objectId.Length == this.ObjectIdColumns.Length)
            {
                for (int idxKeyPart = 0; idxKeyPart < objectId.Length; idxKeyPart++)
                    select.Where.And(PredicateFactory.Compare(this.ObjectIdColumns[idxKeyPart], "=", objectId[idxKeyPart]));
            }
            else
            {
                string semicolDelimitedCompositeKey = ObjectIdToString(objectId);
                select.Where.And(PredicateFactory.Compare(this.ObjectIdColumns[0], "=", semicolDelimitedCompositeKey));
            }

            if (this.ObjectTypeColumn != null)
                select.Where.And(PredicateFactory.Compare(this.ObjectTypeColumn, "=", objectType));

            // Fetch only last/current lock info. There should never exist more than one lock info.
            select.Top = 1;
            select.OrderBy.Add(this.LockedTillColumn, false);
            DataTable lockData = select.Execute();

            LockInfo lastLock = (lockData.Rows.Count > 0) ? new LockInfo(lockData.Rows[0], this) : null;
            return lastLock;
        }

        private void InsertLockInfo(object lockedBy, string objectType, object[] objectId, DateTime lockedTill, object lockMode)
        {
            // Remove obsolete cache entry.
            string cacheKey = GetCacheKey(objectType, objectId);
            if (GlobalCache.Contains(cacheKey))
                GlobalCache.Remove(cacheKey);

            RpcHelper.InvokeMethod(this, "DbInsertLockInfo", lockedBy, objectType, objectId, lockedTill, lockMode);
            
            // Cache only valid locks acquired by the current server.
            LockInfo newLock = new LockInfo(lockedBy, lockedTill, lockMode, this);
            if (this.CacheEnabled && IsActiveLock(newLock) && GetCacheExpirationInMinutes(lockedTill).HasValue)
                GlobalCache.SetData(cacheKey, new LockInfo(lockedBy, lockedTill, lockMode, this), GetCacheExpirationInMinutes(lockedTill).Value, false);
        }

        /// <summary>
        /// This method supports the BizElements.BusinessLayer infrastructure and is not indented to be used in your code.
        /// </summary>
        protected void DbInsertLockInfo(object lockedBy, string objectType, object[] objectId, DateTime lockedTill, object lockMode)
        {
            // Remove obsolete cache entry.
            string cacheKey = GetCacheKey(objectType, objectId);
            if (GlobalCache.Contains(cacheKey))
                GlobalCache.Remove(cacheKey);

            EnsureRequiredPropertiesAreSet();
            if (lockMode != null && this.LockModeColumn == null)
                throw new InvalidOperationException("Cannot specify lock mode if LockModeColumn property is not set.");

            InsertStatement insert = new InsertStatement(this.LockInfoTable);
            insert.AddExpression(this.LockedByColumn, lockedBy);
            insert.AddExpression(this.LockedTillColumn, lockedTill);
            if (this.LockModeColumn != null)
                insert.AddExpression(this.LockModeColumn, lockMode);

            if (objectId.Length == this.ObjectIdColumns.Length)
            {
                for (int idxKeyPart = 0; idxKeyPart < objectId.Length; idxKeyPart++)
                    insert.AddExpression(this.ObjectIdColumns[idxKeyPart], objectId[idxKeyPart]);
            }
            else
            {
                string semicolDelimitedCompositeKey = ObjectIdToString(objectId);
                insert.AddExpression(this.ObjectIdColumns[0], semicolDelimitedCompositeKey);
            }

            if (this.ObjectTypeColumn != null)
                insert.AddExpression(this.ObjectTypeColumn, objectType);

            insert.Execute();

            // Cache only valid locks acquired by the current server.
            LockInfo newLock = new LockInfo(lockedBy, lockedTill, lockMode, this);
            if (this.CacheEnabled && IsActiveLock(newLock) && GetCacheExpirationInMinutes(lockedTill).HasValue)
                GlobalCache.SetData(cacheKey, new LockInfo(lockedBy, lockedTill, lockMode, this), GetCacheExpirationInMinutes(lockedTill).Value, false);
        }

        private void UpdateLockInfo(object lockedBy, string objectType, object[] objectId, DateTime? lockedTill, object lockMode)
        {
            // Remove obsolete cache entry.
            string cacheKey = GetCacheKey(objectType, objectId);
            if (GlobalCache.Contains(cacheKey))
                GlobalCache.Remove(cacheKey);

            RpcHelper.InvokeMethod(this, "DbUpdateLockInfo", lockedBy, objectType, objectId, lockedTill, lockMode);

            // Cache only valid locks acquired by the current server.
            LockInfo newLock = new LockInfo(lockedBy, lockedTill, lockMode, this);
            if (this.CacheEnabled && IsActiveLock(newLock) && GetCacheExpirationInMinutes(lockedTill).HasValue)
                GlobalCache.SetData(cacheKey, new LockInfo(lockedBy, lockedTill, lockMode, this), GetCacheExpirationInMinutes(lockedTill).Value, false);
        }

        /// <summary>
        /// This method supports the BizElements.BusinessLayer infrastructure and is not indented to be used in your code.
        /// </summary>
        protected void DbUpdateLockInfo(object lockedBy, string objectType, object[] objectId, DateTime? lockedTill, object lockMode)
        {
            // Remove obsolete cache entry.
            string cacheKey = GetCacheKey(objectType, objectId);
            if (GlobalCache.Contains(cacheKey))
                GlobalCache.Remove(cacheKey);

            EnsureRequiredPropertiesAreSet();
            if (lockMode != null && this.LockModeColumn == null)
                throw new InvalidOperationException("Cannot specify lock mode if LockModeColumn property is not set.");

            UpdateStatement update = new UpdateStatement(this.LockInfoTable);
            update.UpdateList.Add(this.LockedByColumn, lockedBy);
            update.UpdateList.Add(this.LockedTillColumn, lockedTill);
            if (this.LockModeColumn != null)
                update.UpdateList.Add(this.LockModeColumn, lockMode);

            if (objectId.Length == this.ObjectIdColumns.Length)
            {
                for (int idxKeyPart = 0; idxKeyPart < objectId.Length; idxKeyPart++)
                    update.Where.And(PredicateFactory.Compare(this.ObjectIdColumns[idxKeyPart], "=", objectId[idxKeyPart]));
            }
            else
            {
                string semicolDelimitedCompositeKey = ObjectIdToString(objectId);
                update.Where.And(PredicateFactory.Compare(this.ObjectIdColumns[0], "=", semicolDelimitedCompositeKey));
            }

            if (this.ObjectTypeColumn != null)
                update.Where.And(PredicateFactory.Compare(this.ObjectTypeColumn, "=", objectType));

            update.Execute();

            // Cache only valid locks acquired by the current server.
            LockInfo newLock = new LockInfo(lockedBy, lockedTill, lockMode, this);
            if (this.CacheEnabled && IsActiveLock(newLock) && GetCacheExpirationInMinutes(lockedTill).HasValue)
                GlobalCache.SetData(cacheKey, new LockInfo(lockedBy, lockedTill, lockMode, this), GetCacheExpirationInMinutes(lockedTill).Value, false);
        }

        #endregion

        #region Validation, exceptions.

        private void EnsureRequiredPropertiesAreSet()
        {
            if (this.LockInfoTable == null) throw new InvalidOperationException("LockInfoTable property is not set.");
            if (this.LockedByColumn == null) throw new InvalidOperationException("LockedByColumn property is not set.");
            if (this.LockedTillColumn == null) throw new InvalidOperationException("LockedTillColumn property is not set.");
            if (this.ObjectIdColumns == null) throw new InvalidOperationException("ObjectIdColumns property is set.");
            if (this.ObjectIdColumns.Length == 0) throw new InvalidOperationException("ObjectIdColumns must specify at least one column.");
        }

        #endregion

        #region Conversions.

        private static string ObjectIdToString(object[] objectId)
        {
            string semicolDelimited = ArrayUtil.Concat(objectId, ";");
            return (semicolDelimited.Length > 0) ? semicolDelimited : null;
        }

        private static object[] ParseObjectId(string semicolDelimited)
        {
            object[] objectId;
            if (!string.IsNullOrEmpty(semicolDelimited))
            {
                string[] keyParts = semicolDelimited.Split(';');
                objectId = new object[keyParts.Length];
                keyParts.CopyTo(objectId, 0);
            }
            else
            {
                objectId = null;
            }

            return objectId;
        }

        #endregion
    }
}
